package org.springframework.boot.loader;

import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.ExplodedArchive;
import org.springframework.boot.loader.archive.JarFileArchive;
import org.springframework.boot.loader.util.SystemPropertyUtils;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.util.*;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class HyPropertiesLauncher extends Launcher {

    private static final String DEBUG = "loader.debug";

    /**
     * Properties key for main class. As a manifest entry can also be specified as
     * {@code Start-Class}.
     */
    public static final String MAIN = "loader.main";

    /**
     * Properties key for classpath entries (directories possibly containing jars or
     * jars). Multiple entries can be specified using a comma-separated list. {@code
     * BOOT-INF/classes,BOOT-INF/lib} in the application archive are always used.
     */
    public static final String PATH = "loader.path";

    /**
     * Properties key for home directory. This is the location of external configuration
     * if not on classpath, and also the base path for any relative paths in the
     * {@link #PATH loader path}. Defaults to current working directory (
     * <code>${user.dir}</code>).
     */
    public static final String HOME = "loader.home";

    /**
     * Properties key for default command line arguments. These arguments (if present) are
     * prepended to the main method arguments before launching.
     */
    public static final String ARGS = "loader.args";

    /**
     * Properties key for name of external configuration file (excluding suffix). Defaults
     * to "application". Ignored if {@link #CONFIG_LOCATION loader config location} is
     * provided instead.
     */
    public static final String CONFIG_NAME = "loader.config.name";

    /**
     * Properties key for config file location (including optional classpath:, file: or
     * URL prefix).
     */
    public static final String CONFIG_LOCATION = "loader.config.location";

    /**
     * Properties key for boolean flag (default false) which if set will cause the
     * external configuration properties to be copied to System properties (assuming that
     * is allowed by Java security).
     */
    public static final String SET_SYSTEM_PROPERTIES = "loader.system";

    private static final Pattern WORD_SEPARATOR = Pattern.compile("\\W+");

    private static final String NESTED_ARCHIVE_SEPARATOR = "!" + File.separator;

    private final File home;

    private List<String> paths = new ArrayList<>();

    private final Properties properties = new Properties();

    private Archive parent;

    public HyPropertiesLauncher() {
        try {
            this.home = getHomeDirectory();
            initializeProperties();
            initializePaths();
            this.parent = createArchive();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }

    protected File getHomeDirectory() {
        try {
            return new File(getPropertyWithDefault(HOME, "${user.dir}"));
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }

    private void initializeProperties() throws Exception, IOException {
        List<String> configs = new ArrayList<>();
        if (getProperty(CONFIG_LOCATION) != null) {
            configs.add(getProperty(CONFIG_LOCATION));
        } else {
            String[] names = getPropertyWithDefault(CONFIG_NAME, "loader").split(",");
            for (String name : names) {
                configs.add("file:" + getHomeDirectory() + "/" + name + ".properties");
                configs.add("classpath:" + name + ".properties");
                configs.add("classpath:BOOT-INF/classes/" + name + ".properties");
            }
        }
        for (String config : configs) {
            System.out.println(">>>>>>>>>>>>>config:" + config + "<<<<<<<<<<<<<<<");
            try (InputStream resource = getResource(config)) {
                if (resource != null) {
                    debug("Found: " + config);
                    loadResource(resource);
                    // Load the first one we find
                    return;
                } else {
                    debug("Not found: " + config);
                }
            }
        }
    }

    private void loadResource(InputStream resource) throws IOException, Exception {
        this.properties.load(resource);
        for (Object key : Collections.list(this.properties.propertyNames())) {
            String text = this.properties.getProperty((String) key);
            String value = SystemPropertyUtils.resolvePlaceholders(this.properties, text);
            if (value != null) {
                this.properties.put(key, value);
            }
        }
        if ("true".equals(getProperty(SET_SYSTEM_PROPERTIES))) {
            debug("Adding resolved properties to System properties");
            for (Object key : Collections.list(this.properties.propertyNames())) {
                String value = this.properties.getProperty((String) key);
                System.setProperty((String) key, value);
            }
        }
    }

    private InputStream getResource(String config) throws Exception {
        if (config.startsWith("classpath:")) {
            return getClasspathResource(config.substring("classpath:".length()));
        }
        config = handleUrl(config);
        if (isUrl(config)) {
            return getURLResource(config);
        }
        return getFileResource(config);
    }

    private String handleUrl(String path) throws UnsupportedEncodingException {
        if (path.startsWith("jar:file:") || path.startsWith("file:")) {
            path = URLDecoder.decode(path, "UTF-8");
            if (path.startsWith("file:")) {
                path = path.substring("file:".length());
                if (path.startsWith("//")) {
                    path = path.substring(2);
                }
            }
        }
        return path;
    }

    private boolean isUrl(String config) {
        return config.contains("://");
    }

    private InputStream getClasspathResource(String config) {
        while (config.startsWith("/")) {
            config = config.substring(1);
        }
        config = "/" + config;
        debug("Trying classpath: " + config);
        return getClass().getResourceAsStream(config);
    }

    private InputStream getFileResource(String config) throws Exception {
        File file = new File(config);
        debug("Trying file: " + config);
        if (file.canRead()) {
            return new FileInputStream(file);
        }
        return null;
    }

    private InputStream getURLResource(String config) throws Exception {
        URL url = new URL(config);
        if (exists(url)) {
            URLConnection con = url.openConnection();
            try {
                return con.getInputStream();
            } catch (IOException ex) {
                // Close the HTTP connection (if applicable).
                if (con instanceof HttpURLConnection) {
                    ((HttpURLConnection) con).disconnect();
                }
                throw ex;
            }
        }
        return null;
    }

    private boolean exists(URL url) throws IOException {
        // Try a URL connection content-length header...
        URLConnection connection = url.openConnection();
        try {
            connection.setUseCaches(
                    connection.getClass().getSimpleName().startsWith("JNLP"));
            if (connection instanceof HttpURLConnection) {
                HttpURLConnection httpConnection = (HttpURLConnection) connection;
                httpConnection.setRequestMethod("HEAD");
                int responseCode = httpConnection.getResponseCode();
                if (responseCode == HttpURLConnection.HTTP_OK) {
                    return true;
                } else if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
                    return false;
                }
            }
            return (connection.getContentLength() >= 0);
        } finally {
            if (connection instanceof HttpURLConnection) {
                ((HttpURLConnection) connection).disconnect();
            }
        }
    }

    private void initializePaths() throws Exception {
        String path = getProperty(PATH);
        if (path != null) {
            this.paths = parsePathsProperty(path);
        }
        for(String pathStr : paths){
            System.out.println(">>>>>>>>>>>>>paths[]:" + pathStr + "<<<<<<<<<<<<<<<");
        }
        debug("Nested archive paths: " + this.paths);
    }

    private List<String> parsePathsProperty(String commaSeparatedPaths) {
        List<String> paths = new ArrayList<>();
        for (String path : commaSeparatedPaths.split(",")) {
            path = cleanupPath(path);
            // "" means the user wants root of archive but not current directory
            path = "".equals(path) ? "/" : path;
            paths.add(path);
        }
        if (paths.isEmpty()) {
            paths.add("lib");
        }
        return paths;
    }

    protected String[] getArgs(String... args) throws Exception {
        String loaderArgs = getProperty(ARGS);
        if (loaderArgs != null) {
            String[] defaultArgs = loaderArgs.split("\\s+");
            String[] additionalArgs = args;
            args = new String[defaultArgs.length + additionalArgs.length];
            System.arraycopy(defaultArgs, 0, args, 0, defaultArgs.length);
            System.arraycopy(additionalArgs, 0, args, defaultArgs.length,
                    additionalArgs.length);
        }
        return args;
    }

    @Override
    protected String getMainClass() throws Exception {
        String mainClass = getProperty(MAIN, "Start-Class");
        if (mainClass == null) {
            throw new IllegalStateException(
                    "No '" + MAIN + "' or 'Start-Class' specified");
        }
        System.out.println("========================" + mainClass + "======================");
        return mainClass;
    }

    @Override
    protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
        Set<URL> urls = new LinkedHashSet<>(archives.size());
        for (Archive archive : archives) {
            System.out.println("---------------------1--------------------");
            System.out.println(archive.getUrl());
            System.out.println("---------------------2--------------------");
            urls.add(archive.getUrl());
        }
        ClassLoader loader = new LaunchedURLClassLoader(urls.toArray(new URL[0]),
                getClass().getClassLoader());
        debug("Classpath: " + urls);
        //todo.. 自定义类加载器..
        String customLoaderClassName = getProperty("loader.classLoader");
        if (customLoaderClassName != null) {
            loader = wrapWithCustomClassLoader(loader, customLoaderClassName);
            debug("Using custom class loader: " + customLoaderClassName);
        }
        //todo..
        if(loader!=null){
            System.out.println("__________________loader:" + loader.toString() + "__________________");
            System.out.println("__________________loader com.hy.DubboNacosProviderAPPlication start__________________");
            Class<?> aClass = Class.forName("com.hy.DubboNacosProviderAPPlication", false, loader);
            if(aClass == null){
                System.out.println("+++++++++++++++null+++++++++++++++");
            }else{
                System.out.println("+++++++++++++++" + aClass.toString() + "+++++++++++++++");
            }
            System.out.println("__________________loader com.hy.DubboNacosProviderAPPlication end__________________");
        }
        return loader;
    }

    @SuppressWarnings("unchecked")
    private ClassLoader wrapWithCustomClassLoader(ClassLoader parent,
                                                  String loaderClassName) throws Exception {
        Class<ClassLoader> loaderClass = (Class<ClassLoader>) Class
                .forName(loaderClassName, true, parent);

        try {
            return loaderClass.getConstructor(ClassLoader.class).newInstance(parent);
        } catch (NoSuchMethodException ex) {
            // Ignore and try with URLs
        }
        try {
            return loaderClass.getConstructor(URL[].class, ClassLoader.class)
                    .newInstance(new URL[0], parent);
        } catch (NoSuchMethodException ex) {
            // Ignore and try without any arguments
        }
        return loaderClass.newInstance();
    }

    private String getProperty(String propertyKey) throws Exception {
        return getProperty(propertyKey, null, null);
    }

    private String getProperty(String propertyKey, String manifestKey) throws Exception {
        return getProperty(propertyKey, manifestKey, null);
    }

    private String getPropertyWithDefault(String propertyKey, String defaultValue)
            throws Exception {
        return getProperty(propertyKey, null, defaultValue);
    }

    private String getProperty(String propertyKey, String manifestKey,
                               String defaultValue) throws Exception {
        if (manifestKey == null) {
            manifestKey = propertyKey.replace('.', '-');
            manifestKey = toCamelCase(manifestKey);
        }
        String property = SystemPropertyUtils.getProperty(propertyKey);
        if (property != null) {
            String value = SystemPropertyUtils.resolvePlaceholders(this.properties,
                    property);
            debug("Property '" + propertyKey + "' from environment: " + value);
            return value;
        }
        if (this.properties.containsKey(propertyKey)) {
            String value = SystemPropertyUtils.resolvePlaceholders(this.properties,
                    this.properties.getProperty(propertyKey));
            debug("Property '" + propertyKey + "' from properties: " + value);
            return value;
        }
        try {
            if (this.home != null) {
                // Prefer home dir for MANIFEST if there is one
                Manifest manifest = new ExplodedArchive(this.home, false).getManifest();
                if (manifest != null) {
                    String value = manifest.getMainAttributes().getValue(manifestKey);
                    if (value != null) {
                        debug("Property '" + manifestKey
                                + "' from home directory manifest: " + value);
                        return SystemPropertyUtils.resolvePlaceholders(this.properties,
                                value);
                    }
                }
            }
        } catch (IllegalStateException ex) {
            // Ignore
        }
        // Otherwise try the parent archive
        Manifest manifest = createArchive().getManifest();
        if (manifest != null) {
            String value = manifest.getMainAttributes().getValue(manifestKey);
            if (value != null) {
                debug("Property '" + manifestKey + "' from archive manifest: " + value);
                return SystemPropertyUtils.resolvePlaceholders(this.properties, value);
            }
        }
        return (defaultValue != null)
                ? SystemPropertyUtils.resolvePlaceholders(this.properties, defaultValue)
                : defaultValue;
    }

    @Override
    protected List<Archive> getClassPathArchives() throws Exception {
        List<Archive> lib = new ArrayList<>();
        for (String path : this.paths) {
            for (Archive archive : getClassPathArchives(path)) {
                if (archive instanceof ExplodedArchive) {
                    List<Archive> nested = new ArrayList<>(
                            archive.getNestedArchives(new HyPropertiesLauncher.HyArchiveEntryFilter()));
                    nested.add(0, archive);
                    lib.addAll(nested);
                } else {
                    lib.add(archive);
                }
            }
        }
        addNestedEntries(lib);
        return lib;
    }

    private List<Archive> getClassPathArchives(String path) throws Exception {
        String root = cleanupPath(handleUrl(path));
        List<Archive> lib = new ArrayList<>();
        File file = new File(root);
        if (!"/".equals(root)) {
            if (!isAbsolutePath(root)) {
                file = new File(this.home, root);
            }
            if (file.isDirectory()) {
                debug("Adding classpath entries from " + file);
                Archive archive = new ExplodedArchive(file, false);
                lib.add(archive);
            }
        }
        Archive archive = getArchive(file);
        if (archive != null) {
            debug("Adding classpath entries from archive " + archive.getUrl() + root);
            lib.add(archive);
        }
        List<Archive> nestedArchives = getNestedArchives(root);
        if (nestedArchives != null) {
            debug("Adding classpath entries from nested " + root);
            lib.addAll(nestedArchives);
        }
        return lib;
    }

    private boolean isAbsolutePath(String root) {
        // Windows contains ":" others start with "/"
        return root.contains(":") || root.startsWith("/");
    }

    private Archive getArchive(File file) throws IOException {
        if (isNestedArchivePath(file)) {
            return null;
        }
        String name = file.getName().toLowerCase(Locale.ENGLISH);
        if (name.endsWith(".jar") || name.endsWith(".zip")) {
            return new JarFileArchive(file);
        }
        return null;
    }

    private boolean isNestedArchivePath(File file) {
        return file.getPath().contains(NESTED_ARCHIVE_SEPARATOR);
    }

    private List<Archive> getNestedArchives(String path) throws Exception {
        Archive parent = this.parent;
        String root = path;
        if (!root.equals("/") && root.startsWith("/")
                || parent.getUrl().equals(this.home.toURI().toURL())) {
            // If home dir is same as parent archive, no need to add it twice.
            return null;
        }
        int index = root.indexOf('!');
        if (index != -1) {
            File file = new File(this.home, root.substring(0, index));
            if (root.startsWith("jar:file:")) {
                file = new File(root.substring("jar:file:".length(), index));
            }
            parent = new JarFileArchive(file);
            root = root.substring(index + 1);
            while (root.startsWith("/")) {
                root = root.substring(1);
            }
        }
        if (root.endsWith(".jar")) {
            File file = new File(this.home, root);
            if (file.exists()) {
                parent = new JarFileArchive(file);
                root = "";
            }
        }
        if (root.equals("/") || root.equals("./") || root.equals(".")) {
            // The prefix for nested jars is actually empty if it's at the root
            root = "";
        }
        Archive.EntryFilter filter = new HyPropertiesLauncher.HyPrefixMatchingArchiveFilter(root);
        List<Archive> archives = new ArrayList<>(parent.getNestedArchives(filter));
        if (("".equals(root) || ".".equals(root)) && !path.endsWith(".jar")
                && parent != this.parent) {
            // You can't find the root with an entry filter so it has to be added
            // explicitly. But don't add the root of the parent archive.
            archives.add(parent);
        }
        return archives;
    }

    private void addNestedEntries(List<Archive> lib) {
        // The parent archive might have "BOOT-INF/lib/" and "BOOT-INF/classes/"
        // directories, meaning we are running from an executable JAR. We add nested
        // entries from there with low priority (i.e. at end).
        try {
            lib.addAll(this.parent.getNestedArchives((entry) -> {
                if (entry.isDirectory()) {
                    return entry.getName().equals(JarLauncher.BOOT_INF_CLASSES);
                }
                return entry.getName().startsWith(JarLauncher.BOOT_INF_LIB);
            }));
        } catch (IOException ex) {
            // Ignore
        }
    }

    private String cleanupPath(String path) {
        path = path.trim();
        // No need for current dir path
        if (path.startsWith("./")) {
            path = path.substring(2);
        }
        String lowerCasePath = path.toLowerCase(Locale.ENGLISH);
        if (lowerCasePath.endsWith(".jar") || lowerCasePath.endsWith(".zip")) {
            return path;
        }
        if (path.endsWith("/*")) {
            path = path.substring(0, path.length() - 1);
        } else {
            // It's a directory
            if (!path.endsWith("/") && !path.equals(".")) {
                path = path + "/";
            }
        }
        return path;
    }

    public static void main(String[] args) throws Exception {
        HyPropertiesLauncher launcher = new HyPropertiesLauncher();
        args = launcher.getArgs(args);
        launcher.launch(args);
    }

    public static String toCamelCase(CharSequence string) {
        if (string == null) {
            return null;
        }
        StringBuilder builder = new StringBuilder();
        Matcher matcher = WORD_SEPARATOR.matcher(string);
        int pos = 0;
        while (matcher.find()) {
            builder.append(capitalize(string.subSequence(pos, matcher.end()).toString()));
            pos = matcher.end();
        }
        builder.append(capitalize(string.subSequence(pos, string.length()).toString()));
        return builder.toString();
    }

    private static String capitalize(String str) {
        return Character.toUpperCase(str.charAt(0)) + str.substring(1);
    }

    private void debug(String message) {
        if (Boolean.getBoolean(DEBUG)) {
            System.out.println(message);
        }
    }

    /**
     * Convenience class for finding nested archives that have a prefix in their file path
     * (e.g. "lib/").
     */
    private static final class HyPrefixMatchingArchiveFilter implements Archive.EntryFilter {

        private final String prefix;

        private final HyPropertiesLauncher.HyArchiveEntryFilter filter = new HyPropertiesLauncher.HyArchiveEntryFilter();

        private HyPrefixMatchingArchiveFilter(String prefix) {
            this.prefix = prefix;
        }

        @Override
        public boolean matches(Archive.Entry entry) {
            if (entry.isDirectory()) {
                return entry.getName().equals(this.prefix);
            }
            return entry.getName().startsWith(this.prefix) && this.filter.matches(entry);
        }

    }

    /**
     * Convenience class for finding nested archives (archive entries that can be
     * classpath entries).
     */
    private static final class HyArchiveEntryFilter implements Archive.EntryFilter {

        private static final String DOT_JAR = ".jar";

        private static final String DOT_ZIP = ".zip";

        @Override
        public boolean matches(Archive.Entry entry) {
            return entry.getName().endsWith(DOT_JAR) || entry.getName().endsWith(DOT_ZIP);
        }

    }
}